import memoizeOne from 'memoize-one';

type DayParam = {
  /**
   * 日规则：
   * workDay，工作日
   * lastDay，每月的最后一天
   */
  dayRule: string;
  /**
   * 日规则对应值
   */
  dayRuleSup: string;
  days: number[]
}

type WeekParam = {
  /**
   * 周规则：
   * weekDay，
   * assWeek
   * lastWeek
   */
  weekRule: string;
  /**
   * 日规则对应值
   * 当weekRule为assWeek时，weekRuleSup为两位长度的数组，第一位为第几周，第二位为周几
   * 当weekRule为lastWeek时，weekRuleSup为一位长度的数组，值为周几
   */
  weekRuleSup: number[];
  days: number[]
}

/**
 * 根据cron表达式计算，最近x次的运行时间
 * @param cronExpression cron表达式
 * @returns cron表达式的运行时间列表
 */
export function cronExpressionChange(cronExpression: string) : string[] {
  console.info('cron change ', cronExpression);
  let resultArr: string[] = [];
  if (!cronExpression) {
    console.info('cron表达式为空');
    return [];
  }
  // 按空格分割
  // 0 0/3 * ? * SATL
  const ruleArr: string[] = cronExpression.split(" ");
  // 这里图省事，暂时不做长度校验了

  // 日和周只允许配置一个，要么配置日，要么配置周
  const dayFlag = ruleArr[3] !== '?';
  const weekFlag = ruleArr[5] !== '?';
  // console.info(dayFlag, weekFlag);
  // 配置日，不配置周 dayFlag && !weekFlag
  // 不配置日，配置周 !dayFlag && weekFlag
  if (!((dayFlag && !weekFlag) || (!dayFlag && weekFlag))) {
    resultArr.push('输入的Cron表达式不合法，请重新配置');
    return resultArr;
  }
  // 获取当前时间
  const cDate: Date = new Date();
  let cYear: number = cDate.getFullYear();
  // 月份0-11，会少1，所以这里需要加1
  let cMonth: number = cDate.getMonth() + 1;
  let cDay: number = cDate.getDate();
  // 一周中的某一天，日：0 ,一：1,二：2,三：3,四：4,五：5,六：6
  // let cWeek: number = cDate.getDay();
  let cHour: number = cDate.getHours();
  let cMinute: number = cDate.getMinutes();
  let cSecond: number = cDate.getSeconds();
  // console.info(cSecond, cMinute, cHour, cDay, cMonth, cYear);

  // 获取年月日时分秒的循环数组
  // 年可能不存在，ruleArr[6]值是undefined
  let seconds = memoizedGetSecondArr(ruleArr[0]);
  let minutes = memoizedGetMinuteArr(ruleArr[1]);
  let hours = memoizedGetHourArr(ruleArr[2]);
  let dayParam = memoizedGetDayArr(ruleArr[3]);
  let { days, dayRule, dayRuleSup } = dayParam;
  let weekParam = memoizedGetWeekArr(ruleArr[3], ruleArr[5]);
  let { weekRule, weekRuleSup } = weekParam;
  if (ruleArr[3] === '?') {
    days = weekParam.days;
  }
  let months = memoizedGetMonthArr(ruleArr[4]);
  let years = memoizedGetYearArr(ruleArr[6], cYear);
  // console.info(seconds);
  // console.info(minutes);
  // console.info(hours);
  // console.info(days);
  // console.info(months);
  // console.info(years);

  // 获取当前时间各字段对应的位置
  let secondIndex = getIndex(seconds, cSecond);
  let minuteIndex = getIndex(minutes, cMinute);
  let hourIndex = getIndex(hours, cHour);
  let dayIndex = getIndex(days,cDay);
  let monthIndex = getIndex(months, cMonth);
  let yearIndex = getIndex(years, cYear);
  // console.info(secondIndex, minuteIndex, hourIndex, dayIndex, monthIndex, yearIndex);
  console.info(dayRule, dayRuleSup, weekRule, weekRuleSup);

  const resultNum = 5;
  /**
   * 重置月份及往下所有条件
   * 重置表示当前月份不满足条件，只能从满足条件的月数组从头开始
   * 比如，当前月份为8月，
   * 如果月数组为1，2，9，10，则可以不从头开始，直接取数组的值即可
   * 如果月数组为1，2，则当前月份失去作用，只能从头开始
   */
  const resetMonth = () => {
    monthIndex = 0;
    cMonth = months[0];
    resetDay();
  }
  const resetDay = () => {
    dayIndex = 0;
    cDay = days[0];
    resetHour();
  }
  const resetHour = () => {
    hourIndex = 0;
    cHour = hours[0];
    resetMinute();
  }
  const resetMinute = () => {
    minuteIndex = 0;
    cMinute = minutes[0];
    resetSecond();
  }
  const resetSecond = () => {
    secondIndex = 0;
    cSecond = seconds[0];
  }
  // 当前年份不在年数组中，则说明当前年份不满足条件，从满足条件的数组取一个年份，同时月份也要从头算起
  if (cYear !== years[yearIndex]) {
    resetMonth();
  }
  if (cMonth !== months[monthIndex]) {
    resetDay();
  }
  if (cDay !== days[dayIndex]) {
    resetHour();
  }
  if (cHour !== hours[hourIndex]) {
    resetMinute();
  }
  if (cMinute !== minutes[minuteIndex]) {
    resetSecond();
  }

  goYear: for (let yearI = yearIndex; yearI < years.length; yearI ++) {
    let year = years[yearI];
    // 判断当前月份是否在月数组里
    if (cMonth > months[months.length - 1]) {
      resetMonth();
      continue;
    }
    goMonth: for (let monthI = monthIndex; monthI < months.length; monthI ++) {
      let month = months[monthI];
      // 日相关判断
      // 如果到达最大值时
      if (cDay > days[days.length - 1]) {
        resetDay();
        if (monthI === months.length - 1) {
          resetMonth();
          continue goYear;
        }
        continue;
      }
      goDay: for (let dayI = dayIndex; dayI < days.length; dayI ++) {
        let day = days[dayI];
        if (cHour > hours[hours.length - 1]) {
          resetHour();
          if (dayI === days.length - 1) {
            resetDay();
            if (monthI === months.length - 1) {
              resetMonth();
              continue goYear;
            }
            continue goMonth;
          }
          continue;
        }
        // 需要判断勾选的日期是否合法
        // 判断日期的合法性，不合法的话也是跳出当前循环
        const dayStr = year + '-' + fillZero(month) + '-' + fillZero(day) + ' 00:00:00';
        if (checkDate(dayStr) !== true &&
          (dayRule !== 'workDay' && dayRule !== 'lastDay'
          && weekRule !== 'weekDay' && weekRule !== 'assWeek' && weekRule !== 'lastWeek')) {
          // 可以理解为指定规则下勾选的日期不满足条件
          resetDay();
          continue goMonth;
        }
        if (dayRule === 'lastDay') {
          // 每月的最后一天
          // 如果不是合法日期则需要将前将日期调到合法日期即月末最后一天
          day = getLastDayOfMonth(month, year);
        } else if(dayRule === 'workDay') {
          // 工作日
          // 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
          const lastDay = getLastDayOfMonth(month, year);
          if (day > lastDay) {
            day = lastDay;
          }
          // console.info('workDay', month, day);
          // 获取达到条件的日期是星期X
          let thisWeek = Number(formatDate(new Date(year + '-' + fillZero(month) + '-' + fillZero(day) + ' 00:00:00'), 'week'));
          // console.info('workDay', thisWeek);
          if (thisWeek === 7) {
            // 当日期为星期日时，计算下一天
            // 先找下一个日，并判断是否为月底
            day++;
            if (day > lastDay) {
              // 因上面加1了，所以需要减3才会回到周五
              day -= 3;
            }
          } else if (thisWeek === 6) {
            // 当星期六时，只需判断不是1号就可进行操作
            // 1号为周六，则当月最近的那一天为下周一，所以要加2
            // 否则当月最近的那一天为前一天，所以要减1
            if (Number(dayRuleSup) !== 1) {
              day--;
            } else {
              day += 2;
            }
          }
        } else if (weekRule === 'weekDay') {
          // 如果指定了是星期几
          // 获取当前日期是属于星期几
          let thisWeek = Number(formatDate(new Date(dayStr), 'week'));
          // 校验当前星期是否在星期池（weekRuleSup）中
          // console.info(weekRuleSup, thisWeek, day);
          if (weekRuleSup.indexOf(thisWeek) < 0) {
            // 如果到达最大值时
            if (dayI === days.length - 1) {
              resetDay();
              if (monthI === months.length - 1) {
                resetMonth();
                continue goYear;
              }
              continue goMonth;
            }
            continue;
          }
        } else if (weekRule == 'assWeek') {
          // 如果指定了是第几周的星期几
          // 这里day已经在getWeek方法里固定设置为1号了
          // 获取每月1号是属于星期几
          let thisWeek = Number(formatDate(new Date(dayStr), 'week'));
          if (weekRuleSup[1] >= thisWeek) {
            // 指定的日期在
            day = (weekRuleSup[0] - 1) * 7 + weekRuleSup[1] - thisWeek + 1;
          } else {
            day = weekRuleSup[0] * 7 + weekRuleSup[1] - thisWeek + 1;
          }
        } else if (weekRule == 'lastWeek') {
          // 如果指定了每月最后一个星期几
          const lastDay = getLastDayOfMonth(month, year);
          // 将日期调整为正常的月底那一天
          day = lastDay;
          // 获取月末最后一天是星期几
          let thisWeek = Number(formatDate(new Date(year + '-' + fillZero(month) + '-' + fillZero(lastDay) + ' 00:00:00'), 'week'));
          // 找到要求中最近的那个星期几
          // console.info(weekRuleSup, thisWeek);
          if (weekRuleSup[0] < thisWeek) {
            day -= thisWeek - weekRuleSup[0];
          } else if (weekRuleSup[0] > thisWeek) {
            day -= 7 - (weekRuleSup[0] - thisWeek)
          }
        }
        goHour: for (let hourI = hourIndex; hourI < hours.length; hourI ++) {
          let hour = hours[hourI];
          // 如果到达最大值时
          if (cMinute > minutes[minutes.length - 1]) {
            resetMinute();
            if (hourI === hours.length - 1) {
              resetHour();
              if (dayI === days.length - 1) {
                resetDay();
                if (monthI === months.length - 1) {
                  resetMonth();
                  continue goYear;
                }
                continue goMonth;
              }
              continue goDay;
            }
            continue;
          }
          goMinute: for (let minuteI = minuteIndex; minuteI < minutes.length; minuteI ++) {
            let minute = minutes[minuteI];
            // 如果到达最大值时
            if (cSecond > seconds[seconds.length - 1]) {
              resetSecond();
              if (minuteI === minutes.length - 1) {
                resetMinute();
                if (hourI === hours.length - 1) {
                  resetHour();
                  if (dayI === days.length - 1) {
                    resetDay();
                    if (monthI === months.length - 1) {
                      resetMonth();
                      continue goYear;
                    }
                    continue goMonth;
                  }
                  continue goDay;
                }
                continue goHour;
              }
              continue;
            }
            for (let secondI = secondIndex; secondI < seconds.length; secondI ++) {
              let second = seconds[secondI];
              // console.info(day);
              const result = year + '-' + fillZero(month) + '-' + fillZero(day)
              + ' ' + fillZero(hour) + ':' + fillZero(minute) + ':' + fillZero(second);
              resultArr.push(result);
              // 只取固定数量的结果展示
              if (resultArr.length === resultNum) {
                break goYear;
              }
              // 如果到达最大值时
              if (secondI === seconds.length - 1) {
                resetSecond();
                if (minuteI === minutes.length - 1) {
                  resetMinute();
                  if (hourI === hours.length - 1) {
                    resetHour();
                    if (dayI === days.length - 1) {
                      resetDay();
                      if (monthI === months.length - 1) {
                        resetMonth();
                        continue goYear;
                      }
                      continue goMonth;
                    }
                    continue goDay;
                  }
                  continue goHour;
                }
                continue goMinute;
              }
            }
            // 秒数组遍历完后，没有达到结果条数，将重新开始
          }
        }
      }
    }
  }
  if (resultArr.length === 0) {
    resultArr.push('当前Cron表达式无法计算运行时间');
  } else {
    if (resultArr.length < 5) {
      resultArr.push(`当前Cron表达式只有上面${resultArr.length}条结果`);
    }
  }
  // 清空，防止全局变量被二次使用
  // dayRule = '';
  // weekRule = '';
  return resultArr;
}

/**
 * 用于计算给定的值在数组中的位置，如不存在数组中，则返回数组首位
 * @param arr 数组
 * @param value 给定的值
 * @returns 值在数组中的位置
 */
function getIndex(arr: number[], value: number) {
  // 要计算的值小于等于第一位，或者大于最后一位，则设置为第一位
  if (value <= arr[0] || value > arr[arr.length - 1]) {
    return 0;
  } else {
    // 要计算的值大于某一位置，但小于下一个位置，则设置为下一个位置
    for (let i = 0; i < arr.length - 1; i++) {
      if (value > arr[i] && value <= arr[i + 1]) {
        return i + 1;
      }
    }
    return 0;
  }
}

/**
 * 根据传进来的min-max返回一个顺序的数组
 * @param min 最小值
 * @param max 最大值
 * @returns 数组
 */
function getOrderArr(min: number, max: number) {
  let arr = [];
  for (let i = min; i <= max; i++) {
    arr.push(i);
  }
  return arr;
}

/**
 * 根据规则中指定的零散值返回一个数组
 * @param rule 规则字符串，1，2，3，4
 * @returns 数组
 */
function getAssignArr(rule: string) {
  let arr = [];
  let assiginArr = rule.split(',');
  for (let i = 0; i < assiginArr.length; i++) {
    arr[i] = Number(assiginArr[i])
  }
  // 由于指定时未排序，因此这里需要排序
  arr.sort(compare);
  return arr;
}


/**
 * 根据一定算术规则计算返回一个数组
 * @param rule 规则字符串，1/2 从第1秒开始，每隔2秒执行一次
 * @param limit 最大值
 * @returns 数组
 */
function getAverageArr(rule: string, limit: number) {
  console.info('getAverageArr', rule, limit);
  let arr: number[] = [];
  let ruleArr = rule.split('/');
  let min = Number(ruleArr[0]);
  let step = Number(ruleArr[1]);
  if (step === 0) {
    return arr;
  }
  while (min <= limit) {
    arr.push(min);
    min += step;
  }
  return arr;
}

/**
 * 根据规则返回一个具有周期性的数组
 * @param rule 规则字符串，1-3 从第1秒到第3秒，每秒执行一次
 * @param limit 最大值
 * @param zeroStart 循环后是否从0开始，true-从0开始
 * @param week 周标识
 * @returns 数组
 */
function getCycleArr(rule: string, limit: number, zeroStart: boolean, week: boolean = false) {
  let arr = [];
  let cycleArr = rule.split('-');
  let min = 0;
  let max = 0;
  if (week) {
    min = getDayOfWeek(cycleArr[0]);
    max = getDayOfWeek(cycleArr[1]);
  } else {
    min = Number(cycleArr[0]);
    max = Number(cycleArr[1]);
  }
  if (min > max) {
    max += limit;
  }
  for (let i = min; i <= max; i++) {
    let add = 0;
    if (zeroStart === false && i % limit === 0) {
      add = limit;
    }
    arr.push(Math.round(i % limit + add))
  }
  arr.sort(compare);
  return arr;
}

// 比较数字大小（用于Array.sort）
function compare(value1: number, value2: number) {
  if (value2 - value1 > 0) {
    return -1;
  } else {
    return 1;
  }
}

// 生成可缓存的方法，当参数不变时，不计算直接返回上一次的结果
const memoizedGetYearArr = memoizeOne(getYearArr);
const memoizedGetMonthArr = memoizeOne(getMonthArr);
const memoizedGetWeekArr = memoizeOne(getWeekArr);
const memoizedGetDayArr = memoizeOne(getDayArr);
const memoizedGetHourArr = memoizeOne(getHourArr);
const memoizedGetMinuteArr = memoizeOne(getMinuteArr);
const memoizedGetSecondArr = memoizeOne(getSecondArr);

/**
 * 获取年数组
 * @param rule 年对应的规则
 * @param year 当前年份
 * @returns 年数组
 */
function getYearArr(rule: string, year: number) {
  // console.info('year: ', rule);
  let years: number[] = [];
  if (rule !== undefined) {
    if (rule.indexOf('-') >= 0) {
      years = getCycleArr(rule, year + 10, false);
    } else if (rule.indexOf('/') >= 0) {
      years = getAverageArr(rule, year + 10);
    } else if (rule !== '*') {
      years = getAssignArr(rule);
    }
  } else {
    years = getOrderArr(year, year + 10);
  }
  return years;
}

/**
 * 获取月数组
 * @param rule 月对应的规则
 * @returns 月数组
 */
function getMonthArr(rule: string) {
  let months: number[] = [];
  if (rule.indexOf('-') >= 0) {
    // 月份最大值为12，且循环后，要从1开始算
    months = getCycleArr(rule, 12, false);
  } else if (rule.indexOf('/') >= 0) {
    months = getAverageArr(rule, 12);
  } else if (rule !== '*') {
    months = getAssignArr(rule);
  } else {
    months = getOrderArr(1, 12);
  }
  return months;
}

/**
 * 周规则，实际上还是日规则，在quartz中，周是1-7，1是星期日，7是星期六
 * @param rule 周对应的规则
 * @returns 日数组
 */
function getWeekArr(dRule: string, wRule: string) {
  let days: number[] = [];
  let weekRule = '';
  let weekRuleSup: number[] = [];
  // 日规则为未指定才会处理周规则
  if (dRule === '?') {
    // 只有当日期规则的两个值均为""时，则表达日期是有选项的
    if (wRule.indexOf('-') >= 0) {
      // 周一到周三
      weekRule = 'weekDay';
      // 0-6
      // 星期日到星期一
      weekRuleSup = getCycleArr(wRule, 7, false, true);
    } else if (wRule.indexOf('#') >= 0) {
      // 第1周的周二 1#TUE
      weekRule = 'assWeek';
      const matchRule: string[] = wRule.split('#');
      weekRuleSup = [Number(matchRule[0]), Number(getDayOfWeek(matchRule[1]))];
      // 根据每月1号是星期几，然后往后推算
      days = [1];
    } else if (wRule.indexOf('L') >= 0) {
      // 每月最后一个周三 TUEL
      weekRule = 'lastWeek';
      const weekCode = wRule.replace(/L/g, '');
      weekRuleSup = [Number(getDayOfWeek(weekCode))];
      days = [31];
    } else if (wRule !== '*' && wRule !== '?') {
      weekRule = 'weekDay';
      // 获取周对应的英文缩写，然后转换为数字
      weekRuleSup = wRule.split(',').map((item: string) => {
        return getDayOfWeek(item);
      });
    } else {
      weekRule = 'null';
      weekRuleSup = [];
    }
    if (days.length == 0) {
      days = getOrderArr(1, 31);
    }
  }
  const param: WeekParam = {
    weekRule,
    weekRuleSup,
    days
  }
  return param;
}

/**
 * 获取日数组
 * @param rule 日对应的规则
 * @returns 日数组
 */
function getDayArr(rule: string) {
  let days: number[] = [];
  let dayRule = '';
  let dayRuleSup = '';
  if (rule.indexOf('-') >= 0) {
    dayRule = 'null';
    dayRuleSup = 'null';
    days = getCycleArr(rule, 31, false);
  } else if (rule.indexOf('/') >= 0) {
    dayRule = 'null';
    dayRuleSup = 'null';
    days = getAverageArr(rule, 31);
  } else if (rule.indexOf('W') >= 0) {
    // 13W
    dayRule = 'workDay';
    dayRuleSup = rule.replace(/W/g, '');
    // console.info(dayRule, dayRuleSup);
    // 每月 13 号最近的那个工作日执行一次
    days = [Number(dayRuleSup)];
  } else if (rule.indexOf('L') >= 0) {
    // 每月最后一天
    dayRule = 'lastDay';
    dayRuleSup = 'null';
    // 暂定为31天，具体处理时将换成真正的天数
    days = [31];
  } else if (rule !== '*' && rule !== '?') {
    dayRule = 'null';
    dayRuleSup = 'null';
    days = getAssignArr(rule);
  } else if (rule === '*') {
    dayRule = 'null';
    dayRuleSup = 'null';
    days = getOrderArr(1, 31);
  }
  const param: DayParam = {
    dayRule,
    dayRuleSup,
    days
  }
  return param;
}

/**
 * 获取时数组
 * @param rule 时对应的规则
 * @returns 时数组
 */
function getHourArr(rule: string) {
  let hours: number[] = [];
  if (rule.indexOf('-') >= 0) {
    hours = getCycleArr(rule, 24, true);
  } else if (rule.indexOf('/') >= 0) {
    hours = getAverageArr(rule, 23);
  } else if (rule !== '*') {
    hours = getAssignArr(rule);
  } else {
    hours = getOrderArr(0, 23);
  }
  return hours;
}

/**
 * 获取分数组
 * @param rule 分对应的规则
 * @returns 分数组
 */
function getMinuteArr(rule: string) {
  let minutes: number[] = [];
  if (rule.indexOf('-') >= 0) {
    minutes = getCycleArr(rule, 60, true);
  } else if (rule.indexOf('/') >= 0) {
    minutes = getAverageArr(rule, 59);
  } else if (rule !== '*') {
    minutes = getAssignArr(rule);
  } else {
    minutes = getOrderArr(0, 59);
  }
  return minutes;
}

/**
 * 获取秒数组
 * @param rule 秒对应的规则
 * @returns 秒数组
 */
function getSecondArr(rule: string) {
  console.info('getSecondArr', rule);
  let seconds: number[] = [];
  if (rule.indexOf('-') >= 0) {
    seconds = getCycleArr(rule, 60, true);
  } else if (rule.indexOf('/') >= 0) {
    seconds = getAverageArr(rule, 59);
  } else if (rule !== '*') {
    seconds = getAssignArr(rule);
  } else {
    seconds = getOrderArr(0, 59);
  }
  return seconds;
}

/**
 * 判断年份是否为闰年
 * @param year 年份
 * @returns 是否为闰年，true-是闰年
 */
function isLeapYear(year: number): boolean {
  return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
}

/**
 * 获取月份的最后一天
 * @param month 月份
 * @param year 年份，用于判断2月份的天数
 * @returns 获取月份的最后一天
 */
function getLastDayOfMonth(month: number, year: number): number {
  switch (month) {
    case 1:
      return 31;
    case 2:
      return (isLeapYear(year)) ? 29 : 28;
    case 3:
      return 31;
    case 4:
      return 30;
    case 5:
      return 31;
    case 6:
      return 30;
    case 7:
      return 31;
    case 8:
      return 31;
    case 9:
      return 30;
    case 10:
      return 31;
    case 11:
      return 30;
    case 12:
      return 31;
    default:
      throw new TypeError("Illegal month number: " + month);
  }
}

/**
 * 获取周字符串对应的数字
 * @param week 周
 * @returns 周字符串对应的数字
 */
function getDayOfWeek(week: string): number {
  switch (week) {
    case 'MON':
      return 1;
    case 'TUE':
      return 2;
    case 'WED':
      return 3;
    case 'THU':
      return 4;
    case 'FRI':
      return 5;
    case 'SAT':
      return 6;
    case 'SUN':
      return 7;
    default:
      throw new TypeError("Illegal week string: " + week);
  }
}

/**
 * 数字小于10时，左补一位0
 * @param num 数字
 * @returns 补0后的字符串
 */
function fillZero(num: number): string {
  if (num < 10) {
    return '0' + num;
  }
  return num + '';
}

/**
 * 格式化日期格式如：2017-9-19 18:04:33
 * @param value 日期对象或者周
 * @param type value是否为周
 * @returns 格式化后的字符串
 */
function formatDate(value: number | Date, type?: string) {
  // 计算日期相关值
  let time = typeof value == 'number' ? new Date(value) : value;
  let year = time.getFullYear();
  let month = time.getMonth() + 1;
  let day = time.getDate();
  let hour = time.getHours();
  let minute = time.getMinutes();
  let second = time.getSeconds();
  // 如果传递了type的话
  if (type === 'week') {
    let week = time.getDay();
    if (week == 0) {
      week = 7;
    }
    return week;
  } else {
    return year + '-' + fillZero(month) + '-' + fillZero(day) + ' ' + fillZero(hour) + ':' + fillZero(minute) + ':' + fillZero(second);
  }
}
/**
 * 检查日期是否存在
 * @param value 日期字符串
 * @returns 日期是否存在，true表示存在
 */
function checkDate(value: string) {
  let time = new Date(value);
  let format = formatDate(time)
  return value === format;
}


